﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using DomainDriver.CommonUtilities.Reflectors;
using DomainDriver.DomainModeling.DomainModels;
using DomainDriver.DomainModeling.DomainObjects;
using DomainDriver.DomainModeling.Events;
using DomainDriver.DomainModeling.Repositories;
using DomainDriver.DomainModeling.Queries;

namespace DomainDriver.DomainModeling.Constraints
{
    public class ListRelationConstraint<OWNER_KEY, ELEMENT_KEY, OWNER, ELEMENT> : ConstraintBase
        where OWNER : IKeyedDomainObject<OWNER_KEY, OWNER>
        where ELEMENT : IKeyedDomainObject<ELEMENT_KEY, ELEMENT>
    {
        private string m_OriginalPropertyName;
        private List<PropertyInfo> m_ElementIdCollectionPropertyInfos;

        /// <summary>
        /// Use like "(OWNER dObj) => dObj.Children".
        /// </summary>
        /// <param name="domainModel"></param>
        /// <param name="propertyGetter"></param>
        public ListRelationConstraint(IDomainModel domainModel, Expression<Func<OWNER, ICollection<ELEMENT_KEY>>> propertyGetter)
            : this(domainModel, ClassReflector.GetPropertyName<OWNER, ICollection<ELEMENT_KEY>>(propertyGetter))
        { }

        public ListRelationConstraint(IDomainModel domainModel, string elementIdsPropertyNameOfOwner)
            : base(domainModel)
        {
            m_ElementIdCollectionPropertyInfos = ExtractConstrainedPropertyInfo<OWNER>(elementIdsPropertyNameOfOwner);
            if (m_ElementIdCollectionPropertyInfos == null)
            { throw new ApplicationException(); }
            m_OriginalPropertyName = elementIdsPropertyNameOfOwner;

            OwnerRepository.AddingDomainObject += new AddingDomainObjectDelegate<OWNER_KEY, OWNER>(OwnerRepository_AddingDomainObject);
            OwnerRepository.UpdatingDomainObject += new UpdatingDomainObjectDelegate<OWNER_KEY, OWNER>(OwnerRepository_UpdatingDomainObject);
            ElementRepository.RemovingDomainObject += new RemovingDomainObjectDelegate<ELEMENT_KEY, ELEMENT>(ElementRepository_RemovingDomainObject);
        }

        protected IRepository<ELEMENT_KEY, ELEMENT> ElementRepository
        {
            get { return DomainModel.GetRepository<ELEMENT_KEY, ELEMENT>(); }
        }

        protected IRepository<OWNER_KEY, OWNER> OwnerRepository
        {
            get { return DomainModel.GetRepository<OWNER_KEY, OWNER>(); }
        }

        protected object GetPropertyValueForOwnerInstance(OWNER ownerInstance)
        {
            return ClassReflector.GetNestedPropertyValue(ownerInstance, m_ElementIdCollectionPropertyInfos);
        }

        protected bool IsOwnerChangeValid(OWNER owner)
        {
            if (!Enabled)
            { return true; }

            object listToVerify = GetPropertyValueForOwnerInstance(owner);

            IList<ELEMENT_KEY> elementKeys = (IList<ELEMENT_KEY>)listToVerify;

            // LATER: Replace with call to "Exists(IList<KEY> keys)"
            foreach (ELEMENT_KEY elementKey in elementKeys)
            {
                if (!ElementRepository.Exists(elementKey))
                { return false; }
            }

            return true;
        }

        protected bool IsElementDeleteValid(ELEMENT element)
        {
            if (!Enabled)
            { return true; }

            string parameterName = m_OriginalPropertyName;
            object parameterValue = element.Key;
            ContainsQuery<OWNER> queryForMatchingOwners = new ContainsQuery<OWNER>(parameterName, parameterValue);

            ICollection<OWNER_KEY> matchingOwnerKeys = OwnerRepository.FindKeys(queryForMatchingOwners);
            if (matchingOwnerKeys.Count > 0)
            { return false; }

            return true;
        }

        protected void OwnerRepository_AddingDomainObject(IRepository<OWNER_KEY, OWNER> sender, AddDomainObjectEventArgs<OWNER_KEY, OWNER> args)
        {
            if (!IsOwnerChangeValid(args.ObjectToAdd))
            {
                throw new ApplicationException();
            }
        }

        protected void OwnerRepository_UpdatingDomainObject(IRepository<OWNER_KEY, OWNER> sender, UpdateDomainObjectEventArgs<OWNER_KEY, OWNER> args)
        {
            if (!IsOwnerChangeValid(args.UpdatedObject))
            {
                throw new ApplicationException();
            }
        }

        protected void ElementRepository_RemovingDomainObject(IRepository<ELEMENT_KEY, ELEMENT> sender, RemoveDomainObjectEventArgs<ELEMENT_KEY, ELEMENT> args)
        {
            if (!IsElementDeleteValid(args.ObjectToRemove))
            {
                throw new ApplicationException();
            }
        }
    }
}